/* eslint-disable @typescript-eslint/no-explicit-any */
import { context } from './context';
import { jsxs } from './jsx-runtime';
import { ObjectProps, BaseElement, ElementProps } from './types';

const EL = Symbol();

export type Fn<T> = Omit<
	{
		// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
		[K in keyof T as NonNullable<T[K]> extends Function ? K : never]: T[K];
	},
	'ref'
>;

// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
function isFunction(value: unknown): value is Function {
	return typeof value === 'function';
}

/**
 * Base class of custom component
 */
export abstract class Component<
	P extends ObjectProps<C>,
	C extends Component<P> = any,
	E extends BaseElement = BaseElement,
> {
	private [EL]?: E;
	get el(): E {
		if (!this[EL]) {
			this[EL] = this.render() as E;
		}
		return this[EL];
	}
	/**
	 * Update element, rerender and replace element.
	 */
	protected update() {
		const el = this.render() as E;
		if (this[EL] && this[EL].parentElement) {
			this[EL].parentElement.replaceChild(el, this[EL]);
		}
		this[EL] = el;
	}
	constructor(protected readonly props: P) {}
	/**
	 * Render and return root element of the component.
	 */
	protected abstract render(): Node;

	/**
	 * Set property of the component(this.props).
	 * @param key - Property name
	 * @param value - Property value
	 * @returns - This component
	 */
	set<K extends keyof Fn<P>>(key: K, value: P[K]): this {
		this.props[key] = value;
		return this;
	}

	protected call<K extends keyof Fn<P>>(method: K, ...argc: Parameters<P[K]>): ReturnType<P[K]> | undefined {
		const fn = this.props[method] as unknown;
		if (isFunction(fn)) {
			return fn.call(this, ...argc);
		}
		return;
	}
}

type ElementConstructor<T extends HTMLElement> = new () => T;

export const WebComponents = {
	register<P = object, T extends HTMLElement = HTMLElement>(
		constructor: ElementConstructor<T>,
		name: `${string}-${string}`,
		extend?: string
	): (props: ElementProps<T> & P) => T {
		const tag = name.startsWith('-') ? name.slice(1) : name;
		let options: ElementDefinitionOptions | undefined;
		if (extend) {
			options = { extends: extend };
		}
		context?.customElements.define(tag, constructor, options);
		const createTag = extend ?? tag;
		return function (props: ElementProps<T> & P & { is?: string }): T {
			if (extend) {
				props.is = tag;
			}
			return jsxs(createTag, props) as T;
		};
	},
};
